# Sắp xếp dữ liệu

<CourseFloatingBanner chapter={5}
  classNames="absolute z-10 right-0 top-0"
  notebooks={[
    {label: "Google Colab", value: "https://colab.research.google.com/github/huggingface/notebooks/blob/master/course/vi/chapter5/section3.ipynb"},
    {label: "Aws Studio", value: "https://studiolab.sagemaker.aws/import/github/huggingface/notebooks/blob/master/course/vi/chapter5/section3.ipynb"},
]} />

Hầu hết thời gian, dữ liệu bạn làm việc sẽ chưa được chuẩn bị hoàn hảo cho các mô hình huấn luyện. Trong phần này, chúng ta sẽ khám phá các tính năng khác nhau mà 🤗 Datasets cung cấp để làm sạch các tập dữ liệu của bạn.

<Youtube id="tqfSFcPMgOI"/>

## Sắp xếp dữ liệu của chúng ta

Tương tự như Pandas, 🤗 Datasets cung cấp một số tính năng để thao túng nội dung của `Dataset` và `DatasetDict`. Chúng ta đã gặp phương thức `Dataset.map()` trong [Chương 3](/course/chapter3) và trong phần này, chúng ta sẽ khám phá một số hàm khác theo ý của chúng ta.

Đối với ví dụ này, chúng tôi sẽ sử dụng [Bộ dữ liệu đánh giá thuốc](https://archive.ics.uci.edu/ml/datasets/Drug+Review+Dataset+%28Drugs.com%29) được lưu trữ trên [Kho lưu trữ Học máy UC Irvine](https://archive.ics.uci.edu/ml/index.php), chứa các đánh giá của bệnh nhân về các loại thuốc khác nhau, cùng với tình trạng đang được điều trị và xếp hạng 10 sao về mức độ hài lòng của bệnh nhân.

Trước tiên, chúng ta cần tải xuống và giải nén dữ liệu, có thể thực hiện bằng lệnh `wget` và  `unzip`:

```py
!wget "https://archive.ics.uci.edu/ml/machine-learning-databases/00462/drugsCom_raw.zip"
!unzip drugsCom_raw.zip
```

Vì TSV chỉ là một biến thể của CSV sử dụng dấu tab thay vì dấu phẩy làm dấu phân cách, chúng ta có thể tải các tệp này bằng cách sử dụng tập lệnh tải `csv` và chỉ định đối số `delimiter` trong hàm `load_dataset()` như sau:

```py
from datasets import load_dataset

data_files = {"train": "drugsComTrain_raw.tsv", "test": "drugsComTest_raw.tsv"}
# \t is the tab character in Python
drug_dataset = load_dataset("csv", data_files=data_files, delimiter="\t")
```

Một thực tiễn khi thực hiện bất kỳ loại phân tích dữ liệu nào là lấy một mẫu ngẫu nhiên nhỏ để có thể cảm nhận nhanh về loại dữ liệu bạn đang làm việc. Trong 🤗 Datasets, chúng ta có thể tạo một mẫu ngẫu nhiên bằng cách xâu chuỗi các hàm `Dataset.shuffle()` và `Dataset.select()` với nhau:

```py
drug_sample = drug_dataset["train"].shuffle(seed=42).select(range(1000))
# Xem qua một số ví dụ đầu tiên
drug_sample[:3]
```

```python out
{'Unnamed: 0': [87571, 178045, 80482],
 'drugName': ['Naproxen', 'Duloxetine', 'Mobic'],
 'condition': ['Gout, Acute', 'ibromyalgia', 'Inflammatory Conditions'],
 'review': ['"like the previous person mention, I&#039;m a strong believer of aleve, it works faster for my gout than the prescription meds I take. No more going to the doctor for refills.....Aleve works!"',
  '"I have taken Cymbalta for about a year and a half for fibromyalgia pain. It is great\r\nas a pain reducer and an anti-depressant, however, the side effects outweighed \r\nany benefit I got from it. I had trouble with restlessness, being tired constantly,\r\ndizziness, dry mouth, numbness and tingling in my feet, and horrible sweating. I am\r\nbeing weaned off of it now. Went from 60 mg to 30mg and now to 15 mg. I will be\r\noff completely in about a week. The fibro pain is coming back, but I would rather deal with it than the side effects."',
  '"I have been taking Mobic for over a year with no side effects other than an elevated blood pressure.  I had severe knee and ankle pain which completely went away after taking Mobic.  I attempted to stop the medication however pain returned after a few days."'],
 'rating': [9.0, 3.0, 10.0],
 'date': ['September 2, 2015', 'November 7, 2011', 'June 5, 2013'],
 'usefulCount': [36, 13, 128]}
```

Lưu ý rằng chúng ta đã sửa seed trong `Dataset.shuffle()` cho mục đích tái tạo.  `Dataset.select()` mong đợi một chỉ số có thể lặp lại, vì vậy chúng ta truyền vào khoảng `range(1000)` để lấy 1,000 mẫu đầu tiên từ tập dữ liệu đã xáo trộn. Từ mẫu này, ta đã có thể thấy một số điều kỳ quặc trong tập dữ liệu:

* Cột `Unnamed: 0`  trông đáng ngờ giống như một ID ẩn danh cho mỗi bệnh nhân.
* Cột `condition` bao gồm sự kết hợp giữa các nhãn chữ hoa và chữ thường.
* Các bài đánh giá có độ dài khác nhau và chứa hỗn hợp các dấu phân tách dòng Python (`\r\n`) cũng như các mã ký tự HTML như `&\#039;`.

Hãy xem cách chúng ta có thể sử dụng 🤗 Datasets để giải quyết từng vấn đề này. Để kiểm tra giả thuyết ID bệnh nhân cho cột `Unnamed: 0`, ta có thể sử dụng hàm  `Dataset.unique()` để xác minh rằng số lượng ID khớp với số hàng trong mỗi lần tách:

```py
for split in drug_dataset.keys():
    assert len(drug_dataset[split]) == len(drug_dataset[split].unique("Unnamed: 0"))
```
Điều này dường như xác nhận giả thuyết của chúng tôi, vì vậy hãy dọn dẹp tập dữ liệu một chút bằng cách đổi tên cột `Unname: 0` thành một cái gì đó dễ hiểu hơn một chút. Chúng ta có thể sử dụng hàm `DatasetDict.rename_column()` để đổi tên cột trên cả hai tập con trong một lần:

```py
drug_dataset = drug_dataset.rename_column(
    original_column_name="Unnamed: 0", new_column_name="patient_id"
)
drug_dataset
```

```python out
DatasetDict({
    train: Dataset({
        features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount'],
        num_rows: 161297
    })
    test: Dataset({
        features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount'],
        num_rows: 53766
    })
})
```

> [!TIP]
> ✏️ **Thử nghiệm thôi!** Sử dụng hàm `Dataset.unique()` để tìm số lượng thuốc độc nhất và điều kiện trong tập huấn luyện và kiểm thử.

Tiếp theo, hãy chuẩn hóa tất cả các nhãn `condition` bằng cách sử dụng `Dataset.map()`. Như chúng ta đã làm với tokenize trong [Chương 3](/course/chapter3), chúng ta có thể xác định một hàm đơn giản có thể được áp dụng trên tất cả các hàng của mỗi tập trong `drug_dataset`:

```py
def lowercase_condition(example):
    return {"condition": example["condition"].lower()}


drug_dataset.map(lowercase_condition)
```

```python out
AttributeError: 'NoneType' object has no attribute 'lower'
```

Ồ không, chúng ta đã gặp sự cố với chức năng nối của mình! Từ lỗi, chúng ta có thể suy ra rằng một số mục nhập trong cột `condition` là `None`, không thể viết thường vì chúng không phải là chuỗi. Hãy bỏ các hàng này bằng cách sử dụng `Dataset.filter()`, hoạt động theo cách tương tự như `Dataset.map()` và mong đợi một hàm nhận được một mẫu về tập dữ liệu. Thay vì viết một hàm rõ ràng như:

```py
def filter_nones(x):
    return x["condition"] is not None
```

và sau đó chạy `drug_dataset.filter(filter_nones)`, chúng ta có thể thực hiện việc này trong một dòng bằng cách sử dụng _hàm lambda_. Trong Python, các hàm lambda là các hàm nhỏ mà bạn có thể định nghĩa mà không cần đặt tên rõ ràng. Chúng có dạng chung:

```
lambda <arguments> : <expression>
```

ở đây `lambda` là một trong những [từ khóa](https://docs.python.org/3/reference/lexical_analysis.html#keywords) đặc biệt của Python, `<arguments>` là danh sách / tập hợp các giá trị được phân tách bằng dấu phẩy xác định các đầu vào cho hàm và `<expression>`  đại diện cho các hoạt động bạn muốn thực hiện. Ví dụ, chúng ta có thể định nghĩa một hàm lambda đơn giản bình phương một số như sau:

```
lambda x : x * x
```

Để áp dụng hàm này cho một đầu vào, chúng ta cần đặt nó và đầu vào trong dấu ngoặc đơn:

```py
(lambda x: x * x)(3)
```

```python out
9
```

Tương tự, chúng ta có thể định nghĩa các hàm lambda với nhiều tham số bằng cách phân tách chúng bằng dấu phẩy. Ví dụ, chúng ta có thể tính diện tích của một tam giác như sau:

```py
(lambda base, height: 0.5 * base * height)(4, 8)
```

```python out
16.0
```

Các hàm Lambda rất hữu ích khi bạn muốn định nghĩa các hàm nhỏ, sử dụng một lần (để biết thêm thông tin về chúng, chúng tôi khuyên bạn nên đọc [Hướng dẫn Python đích thực](https://realpython.com/python-lambda/) của Andre Burgaud). Trong ngữ cảnh 🤗 Datasets, chúng ta có thể sử dụng các hàm lambda để xác định các hoạt động nối và lọc đơn giản, vì vậy hãy sử dụng thủ thuật này để loại bỏ các phần `None` trong tập dữ liệu:

```py
drug_dataset = drug_dataset.filter(lambda x: x["condition"] is not None)
```

Với `None` đã bị xóa, chúng ta  có thể chuẩn hóa cột `condition`:

```py
drug_dataset = drug_dataset.map(lowercase_condition)
# Kiểm tra xem chữ viết thường đã hoạt động chưa
drug_dataset["train"]["condition"][:3]
```

```python out
['left ventricular dysfunction', 'adhd', 'birth control']
```

Nó hoạt động! Vậy là chúng ta đã làm sạch các nhãn, giờ chúng ta hãy xem xét việc làm sạch các bài đánh giá.

## Tạo ra các cột mới

Bất cứ khi nào bạn xử lý các bài đánh giá của khách hàng, một phương pháp hay đó là kiểm tra số lượng từ trong mỗi bài đánh giá. Bài đánh giá có thể chỉ là một từ duy nhất như "Tuyệt vời!" hoặc một bài luận đầy đủ với hàng nghìn từ, và tùy thuộc vào trường hợp sử dụng, bạn sẽ cần xử lý những thái cực này theo cách khác nhau. Để tính toán số lượng từ trong mỗi bài đánh giá, chúng tôi sẽ sử dụng phương pháp phỏng đoán sơ bộ dựa trên việc tách từng văn bản theo khoảng trắng.

Hãy định nghĩa một hàm đơn giản đếm số từ trong mỗi bài đánh giá:

```py
def compute_review_length(example):
    return {"review_length": len(example["review"].split())}
```

Không giống như hàm `lowercase_condition()`, `compute_review_length()` trả về một từ điển có khóa không tương ứng với một trong các tên cột trong tập dữ liệu. Trong trường hợp này, khi `compute_review_length()` được truyền vào `Dataset.map()`, nó sẽ được áp dụng cho tất cả các hàng trong tập dữ liệu để tạo cột mới `review_length`:

```py
drug_dataset = drug_dataset.map(compute_review_length)
# Kiểm tra mẫu huấn luyện đầu tiên
drug_dataset["train"][0]
```

```python out
{'patient_id': 206461,
 'drugName': 'Valsartan',
 'condition': 'left ventricular dysfunction',
 'review': '"It has no side effect, I take it in combination of Bystolic 5 Mg and Fish Oil"',
 'rating': 9.0,
 'date': 'May 20, 2012',
 'usefulCount': 27,
 'review_length': 17}
```

Như mong đợi, chúng ta có thể thấy cột `review_length` đã được thêm vào tập huấn luyện của chúng ta. Chúng ta có thể sắp xếp cột mới này với `Dataset.sort()` để xem các giá trị cực đại trông như thế nào:

```py
drug_dataset["train"].sort("review_length")[:3]
```

```python out
{'patient_id': [103488, 23627, 20558],
 'drugName': ['Loestrin 21 1 / 20', 'Chlorzoxazone', 'Nucynta'],
 'condition': ['birth control', 'muscle spasm', 'pain'],
 'review': ['"Excellent."', '"useless"', '"ok"'],
 'rating': [10.0, 1.0, 6.0],
 'date': ['November 4, 2008', 'March 24, 2017', 'August 20, 2016'],
 'usefulCount': [5, 2, 10],
 'review_length': [1, 1, 1]}
```

Như ta đã nghi vấn, một số đánh giá chỉ chứa một từ duy nhất, mặc dù có thể ổn để phân tích sắc thái, nhưng sẽ không có nhiều thông tin nếu chúng tôi muốn dự đoán tình trạng bệnh.

> [!TIP]
> 🙋 Một cách thay thế để thêm các cột mới vào tập dữ liệu là sử dụng hàm `Dataset.add_column()`. Điều này cho phép bạn cung cấp cột dưới dạng danh sách Python hoặc mảng NumPy và có thể hữu ích trong các trường hợp mà `Dataset.map()` không phù hợp cho phân tích của bạn.

Hãy sử dụng hàm `Dataset.filter()` để xóa các bài đánh giá có ít hơn 30 từ. Tương tự như những gì chúng ta đã làm với cột `condition`, chúng ta có thể lọc ra các bài đánh giá rất ngắn bằng cách yêu cầu các bài đánh giá có độ dài trên ngưỡng này:

```py
drug_dataset = drug_dataset.filter(lambda x: x["review_length"] > 30)
print(drug_dataset.num_rows)
```

```python out
{'train': 138514, 'test': 46108}
```

Như bạn có thể thấy, điều này đã loại bỏ khoảng 15% bài đánh giá khỏi bộ huấn luyện và kiểm thử ban đầu.

> [!TIP]
> ✏️ **Thử nghiệm thôi!** Sử dụng hàm `Dataset.sort()` để kiểm tra các bài đánh giá có số lượng từ lớn nhất. Tham khảo [tài liệu](https://huggingface.co/docs/datasets/package_reference/main_classes#datasets.Dataset.sort) để biết bạn cần sử dụng tham số nào để sắp xếp các bài đánh giá theo thứ tự giảm dần.

Điều cuối cùng chúng ta cần giải quyết là sự hiện diện của ký tự HTML trong các bài đánh giá của chúng ta. Chúng ta có thể sử dụng mô-đun `html` của Python để loại bỏ qua các ký tự này, như sau:

```py
import html

text = "I&#039;m a transformer called BERT"
html.unescape(text)
```

```python out
"I'm a transformer called BERT"
```

Ta sẽ sử dụng `Dataset.map()` để hủy tất cả các ký tự HTML trong kho tài liệu của mình:

```python
drug_dataset = drug_dataset.map(lambda x: {"review": html.unescape(x["review"])})
```

Như bạn có thể thấy, phương thức `Dataset.map()` khá hữu ích để xử lý dữ liệu - và chúng ta thậm chí còn chưa rõ tất mọi thứ mà nó có thể làm!

## Siêu sức mạnh của hàm `map()`

Phương thức `Dataset.map ()` nhận tham số `batched`, nếu được đặt thành `True`, nó sẽ gửi một loạt các mẫu đến hàm map cùng một lúc (ta có thể cấu hình kích thước lô nhưng mặc định là 1,000). Ví dụ: hàm map trước đó loại bỏ tất cả HTML đã mất một chút thời gian để chạy (bạn có thể đọc thời gian thực hiện từ các thanh tiến trình). Chúng ta có thể tăng tốc độ này bằng cách xử lý một số phần tử cùng lúc thông qua sử dụng bao hàm.

Khi bạn chỉ định `batched=True`, hàm sẽ nhận một từ điển với các trường của tập dữ liệu, nhưng mỗi giá trị bây giờ là một _danh sách các giá trị_ và không chỉ là một giá trị duy nhất. Giá trị trả về của `Dataset.map()` phải giống nhau: một từ điển với các trường ta muốn cập nhật hoặc thêm vào tập dữ liệu của mình và một danh sách các giá trị. Ví dụ: đây là một cách khác để hủy tất cả các ký tự HTML, nhưng sử dụng `batched=True`:

```python
new_drug_dataset = drug_dataset.map(
    lambda x: {"review": [html.unescape(o) for o in x["review"]]}, batched=True
)
```

Nếu bạn đang chạy đoạn mã này trên notebook, bạn sẽ thấy rằng lệnh này thực thi nhanh hơn lệnh trước đó. Và đó không phải là do các bài đánh giá của chúng tôi đã được loại đi HTML - nếu bạn thực hiện lại hướng dẫn từ phần trước (không có `batch = True`), nó sẽ mất cùng một khoảng thời gian như trước. Điều này là do việc bao hàm thường nhanh hơn việc thực thi cùng một đoạn mã trong vòng lặp `for` và chúng ta cũng đạt được một số hiệu suất bằng cách truy cập nhiều phần tử cùng một lúc thay vì từng phần tử một.

Sử dụng `Dataset.map()` với `batched=True` sẽ là điều cần thiết để mở khóa tốc độ của các trình tokenize "nhanh" mà chúng ta sẽ gặp trong [Chương 6](/course/chap6), có thể nhanh chóng tokenize các danh sách lớn các văn bản. Ví dụ: để tokenize tất cả các đánh giá thuốc bằng trình tokenize nhanh, ta có thể sử dụng một hàm như sau:

```python
from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")


def tokenize_function(examples):
    return tokenizer(examples["review"], truncation=True)
```

Như bạn đã thấy trong [Chương 3](/course/chapter3), chúng ta có thể truyền vào một hoặc nhiều mẫu cho tokenizer, vì vậy ta có thể sử dụng hàm này với `batched=True` hoặc không. Hãy cũng coi đây là một cơ hội để so sánh hiệu năng của hai tuỳ chọn này. Trong một notebook, bạn có thể bấm giờ chỉ với một dòng lệnh `%time` trước dòng mã bạn muốn tình thời gian:

```python no-format
%time tokenized_dataset = drug_dataset.map(tokenize_function, batched=True)
```

Bạn cũng có thể tính thời gian cho toàn bộ ô bằng cách đặt `%%time` ở đầu của ô mã. Trên phần cứng mà chúng ta thực hiện, nó hiển thị 10.8 giây cho lệnh này (đó là số được viết sau "Wall time").

> [!TIP]
> ✏️ **Thử nghiệm thôi!** Thực hiện cùng một hướng dẫn có và không có `batched=True`, sau đó thử nó với tokenizer chậm (thêm `use_fast=False` vào `AutoTokenizer.from_pretrained()`) để bạn có thể thấy giá trị bạn nhận được trên phần cứng của mình.

Dưới đây là kết quả thu được khi có và không có tính năng phân lô, với tokenizer nhanh và chậm:

Tuỳ chọn         | Tokenizer nhanh | Tokenizer chậm
:--------------:|:--------------:|:-------------:
`batched=True`  | 10.8s          | 4min41s
`batched=False` | 59.2s          | 5min3s

Điều này có nghĩa là việc sử dụng một tokenizer nhanh với tùy chọn `batched=True` sẽ nhanh hơn 30 lần so với phiên bản chậm mà không có lô - điều này thực sự tuyệt vời! Đó là lý do chính tại sao tokenizer nhanh là mặc định khi sử dụng `AutoTokenizer` (và tại sao chúng được gọi là "nhanh"). Chúng có thể đạt được tốc độ như vậy bởi vì phía sau, đoạn mã token hóa được thực thi bằng Rust, đây là một ngôn ngữ giúp dễ dàng thực hiện đoạn mã song song.

Song song hóa cũng là lý do giải thích cho tốc độ tăng gần gấp 6 lần mà trình tokenize nhanh đạt được với việc phân lô: bạn không thể song song một thao tác tokenize đơn lẻ, nhưng khi bạn muốn tokenize nhiều văn bản cùng một lúc, bạn có thể chỉ cần chia nhỏ việc thực thi trên nhiều quy trình, mỗi người chịu trách nhiệm về các văn bản của riêng mình.

`Dataset.map()` cũng tự có một số khả năng tính toán song song. Vì chúng không được hỗ trợ bởi Rust, nên chúng sẽ không để một trình tokenizer chậm bắt kịp với một tokenizer nhanh, nhưng chúng vẫn có thể hữu ích (đặc biệt nếu bạn đang sử dụng một tokenizer không có phiên bản nhanh). Để bật xử lý đa luồng, hãy sử dụng tham số `num_proc` và chỉ định số lượng quy trình sẽ sử dụng trong lệnh gọi của bạn tới `Dataset.map()`:

```py
slow_tokenizer = AutoTokenizer.from_pretrained("bert-base-cased", use_fast=False)


def slow_tokenize_function(examples):
    return slow_tokenizer(examples["review"], truncation=True)


tokenized_dataset = drug_dataset.map(slow_tokenize_function, batched=True, num_proc=8)
```

Bạn có thể thử nghiệm một chút với thời gian để xác định số lượng quy trình tối ưu để sử dụng; trong trường hợp của chúng ta, 8 dường như tạo ra tốc độ tăng tốt nhất. Dưới đây là những con số chúng tôi nhận được khi có và không có xử lý đa luồng:

Tuỳ chọn         | Tokenizer nhanh | Tokenizer chậm
:--------------:|:--------------:|:-------------:
`batched=True`  | 10.8s          | 4min41s
`batched=False` | 59.2s          | 5min3s
`batched=True`, `num_proc=8`  | 6.52s          | 41.3s
`batched=False`, `num_proc=8` | 9.49s          | 45.2s

Đó là những kết quả hợp lý hơn nhiều đối với tokenizer chậm, nhưng hiệu suất của tokenizer nhanh cũng đã được cải thiện đáng kể. Tuy nhiên, lưu ý rằng điều đó không phải lúc nào cũng đúng - đối với các giá trị của `num_proc` khác 8, các thử nghiệm của chúng tôi cho thấy rằng sử dụng `batched=True` mà không có tùy chọn này sẽ nhanh hơn. Nói chung, chúng tôi khuyên bạn không nên sử dụng xử lý đa luồng Python cho các trình tokenize nhanh với `batched=True`.

> [!TIP]
> Sử dụng `num_proc` để tăng tốc quá trình xử lý của bạn thường là một ý tưởng tuyệt vời, miễn là hàm bạn đang sử dụng chưa thực hiện một số kiểu xử lý đa xử lý của riêng nó.

Tất cả các chức năng này được cô đọng trong một phương pháp đã khá tuyệt vời, nhưng còn nhiều hơn thế nữa! Với `Dataset.map()` và `batched=True`, bạn có thể thay đổi số lượng phần tử trong tập dữ liệu của mình. Điều này cực kỳ hữu ích trong nhiều trường hợp mà bạn muốn tạo một số đặc trưng huấn luyện từ một mẫu và chúng ta sẽ cần thực hiện điều này như một phần của quá trình tiền xử lý cho một số tác vụ NLP sẽ thực hiện trong [Chương 7](/course/chapter7).

> [!TIP]
> 💡 Trong học máy, một _mẫu_ thường được định nghĩa là tập hợp _đặc trưng_ mà chúng ta cung cấp cho mô hình. Trong một số ngữ cảnh, các đặc trưng này sẽ là tập hợp thành các cột trong `Dataset`, nhưng trong các trường hợp khác (như ở đây và để phục vụ hỏi đáp), nhiều đặc trưng có thể được trích xuất từ một mẫu và thuộc về một cột duy nhất.

Chúng ta hãy xem nó hoạt động như thế nào! Ở đây, ta sẽ tokenize các mẫu của mình và cắt chúng về độ dài tối đa là 128, nhưng ta sẽ yêu cầu trình tokenize trả về *tất cả* các đoạn văn bản thay vì chỉ đoạn văn bản đầu tiên. Điều này có thể được thực hiện với `return_overflowing_tokens=True`:

```py
def tokenize_and_split(examples):
    return tokenizer(
        examples["review"],
        truncation=True,
        max_length=128,
        return_overflowing_tokens=True,
    )
```

Hãy kiểm tra điều này trên một mẫu trước khi sử dụng `Dataset.map()` trên toàn bộ tập dữ liệu:

```py
result = tokenize_and_split(drug_dataset["train"][0])
[len(inp) for inp in result["input_ids"]]
```

```python out
[128, 49]
```

Vì vậy, mẫu đầu tiên trong tập huấn luyện đã trở thành hai đặc trưng vì nó đã được tokenize nhiều hơn số lượng token tối đa mà chúng tôi đã chỉ định: cái đầu tiên có độ dài 128 và cái thứ hai có độ dài 49. Bây giờ hãy làm điều này cho tất cả các phần tử của tập dữ liệu!

```py
tokenized_dataset = drug_dataset.map(tokenize_and_split, batched=True)
```

```python out
ArrowInvalid: Column 1 named condition expected length 1463 but got length 1000
```

Ôi không! Nó đã không hoạt động! Tại sao không? Nhìn vào thông báo lỗi sẽ cho chúng ta manh mối: có sự không khớp về độ dài của một trong các cột, một cột có độ dài 1,463 và cột còn lại có độ dài 1,000. Nếu bạn đã xem [tài liệu](https://huggingface.co/docs/datasets/package_reference/main_classes#datasets.Dataset.map) về `Dataset.map()`, bạn có thể nhớ rằng đó là số các mẫu được truyền vào hàm mà chúng ta đang ánh xạ; ở đây 1,000 mẫu đó đã cung cấp 1,463 đặc trưng mới, dẫn đến lỗi hình dạng.

Vấn đề là chúng ta đang cố gắng kết hợp hai tập dữ liệu khác nhau với các kích thước khác nhau: cột `drug_dataset` sẽ có một số mẫu nhất định (lỗi phía chúng ta là 1,000), nhưng `tokenized_dataset`  mà chúng ta đang xây dựng sẽ có nhiều hơn (1,463 trong thông báo lỗi). Điều này không hoạt động đối với `Dataset`, vì vậy chúng ta cần xóa các cột khỏi tập dữ liệu cũ hoặc làm cho chúng có cùng kích thước với chúng trong tập dữ liệu mới. Chúng ta có thể thực hiện điều đầu thông qua tham số `remove_columns`:

```py
tokenized_dataset = drug_dataset.map(
    tokenize_and_split, batched=True, remove_columns=drug_dataset["train"].column_names
)
```

Bây giờ nó hoạt động mà không có lỗi. Chúng ta có thể kiểm tra xem tập dữ liệu mới của mình có nhiều phần tử hơn tập dữ liệu gốc hay không bằng cách so sánh độ dài:

```py
len(tokenized_dataset["train"]), len(drug_dataset["train"])
```

```python out
(206772, 138514)
```

Chúng tôi đã đề cập rằng chúng ta cũng có thể giải quyết vấn đề chiều dài không khớp bằng cách làm cho các cột cũ có cùng kích thước với các cột mới. Để thực hiện việc này, chúng ta sẽ cần trường `overflow_to_sample_mapping` mà tokenizer trả về khi chúng ta đặt `return_overflowing_tokens=True`. Nó cung cấp cho chúng ta một ánh xạ từ một chỉ mục đặc trưng mới đến chỉ mục của mẫu mà nó bắt nguồn từ đó. Sử dụng điều này, chúng ta có thể liên kết mỗi khóa có trong tập dữ liệu ban đầu với một danh sách các giá trị có kích thước phù hợp bằng cách lặp lại các giá trị của mỗi ví dụ nhiều lần khi nó tạo ra các đặc trưng mới:

```py
def tokenize_and_split(examples):
    result = tokenizer(
        examples["review"],
        truncation=True,
        max_length=128,
        return_overflowing_tokens=True,
    )
    # Extract mapping between new and old indices
    sample_map = result.pop("overflow_to_sample_mapping")
    for key, values in examples.items():
        result[key] = [values[i] for i in sample_map]
    return result
```

Chúng ta có thể thấy nó hoạt động với `Dataset.map()` mà chúng ta không cần xóa các cột cũ:

```py
tokenized_dataset = drug_dataset.map(tokenize_and_split, batched=True)
tokenized_dataset
```

```python out
DatasetDict({
    train: Dataset({
        features: ['attention_mask', 'condition', 'date', 'drugName', 'input_ids', 'patient_id', 'rating', 'review', 'review_length', 'token_type_ids', 'usefulCount'],
        num_rows: 206772
    })
    test: Dataset({
        features: ['attention_mask', 'condition', 'date', 'drugName', 'input_ids', 'patient_id', 'rating', 'review', 'review_length', 'token_type_ids', 'usefulCount'],
        num_rows: 68876
    })
})
```

Chúng ta nhận được cùng số đặc trưng huấn luyện như trước đó, nhưng ở đây ta đã giữ lại tất cả các trường cũ. Nếu bạn cần chúng để hậu xử lý sau khi áp dụng mô hình của mình, bạn có thể muốn sử dụng phương pháp này.

Bây giờ bạn đã thấy cách 🤗 Datasets có thể được sử dụng để tiền xử lý một tập dữ liệu theo nhiều cách khác nhau. Mặc dù các chức năng xử lý của 🤗 Datasets sẽ đáp ứng hầu hết các nhu cầu huấn luyện mô hình của bạn,
có thể đôi khi bạn cần chuyển sang Pandas để truy cập các tính năng mạnh mẽ hơn, chẳng hạn như  `DataFrame.groupby()` hoặc các API cấp cao để trực quan hóa. May mắn thay, 🤗 Datasets được thiết kế để có thể tương tác với các thư viện như Pandas, NumPy, PyTorch, TensorFlow và JAX. Chúng ta hãy xem cách này hoạt động như thế nào.

## Từ `Dataset` tới `DataFrame` và ngược lại

<Youtube id="tfcY1067A5Q"/>

Để cho phép chuyển đổi giữa các thư viện bên thứ ba khác nhau, 🤗 Datasets cung cấp hàm `Dataset.set_format()`. Hàm này chỉ thay đổi _định dạng đầu ra_ của tập dữ liệu, vì vậy bạn có thể dễ dàng chuyển sang định dạng khác mà không ảnh hưởng đến _định dạng đầu ra_ bên dưới, đó là Apache Arrow. Việc định dạng được thực hiện tại chỗ. Để chứng minh, hãy chuyển đổi tập dữ liệu của chúng tôi thành Pandas:

```py
drug_dataset.set_format("pandas")
```

Giờ khi chúng ta truy cập các phần tử của tập dữ liệu, ta nhận được `pandas.DataFrame` thay vì từ điển:

```py
drug_dataset["train"][:3]
```

<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>patient_id</th>
      <th>drugName</th>
      <th>condition</th>
      <th>review</th>
      <th>rating</th>
      <th>date</th>
      <th>usefulCount</th>
      <th>review_length</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>95260</td>
      <td>Guanfacine</td>
      <td>adhd</td>
      <td>"My son is halfway through his fourth week of Intuniv..."</td>
      <td>8.0</td>
      <td>April 27, 2010</td>
      <td>192</td>
      <td>141</td>
    </tr>
    <tr>
      <th>1</th>
      <td>92703</td>
      <td>Lybrel</td>
      <td>birth control</td>
      <td>"I used to take another oral contraceptive, which had 21 pill cycle, and was very happy- very light periods, max 5 days, no other side effects..."</td>
      <td>5.0</td>
      <td>December 14, 2009</td>
      <td>17</td>
      <td>134</td>
    </tr>
    <tr>
      <th>2</th>
      <td>138000</td>
      <td>Ortho Evra</td>
      <td>birth control</td>
      <td>"This is my first time using any form of birth control..."</td>
      <td>8.0</td>
      <td>November 3, 2015</td>
      <td>10</td>
      <td>89</td>
    </tr>
  </tbody>
</table>

Hãy tạo ra một `pandas.DataFrame` cho toàn bộ tập huấn luyện bằng cách chọn tất cả các phần tử trong `drug_dataset["train"]`:

```py
train_df = drug_dataset["train"][:]
```

> [!TIP]
> 🚨 Bên dưới `Dataset.set_format()` thay đổi định dạng trả về cho phương thức `__getitem __()` của tập dữ liệu. Điều này có nghĩa là khi chúng ta muốn tạo một đối tượng mới như `train_df` từ `Dataset` ở định dạng `"pandas"`, chúng ta cần cắt toàn bộ tập dữ liệu để có được một `pandas.DataFrame`. Bạn có thể tự xác minh xem kiểu dữ liệu của `drug_dataset["train"]` có phải là `Dataset`, bất kể định dạng đầu ra là gì.

Từ đây, ta có thể sử dụng tất cả các chức năng của Pandas mà ta muốn. Ví dụ, chúng ta có thể thực hiện chuỗi lạ mắt để tính toán phân phối lớp giữa các `condition`:

```py
frequencies = (
    train_df["condition"]
    .value_counts()
    .to_frame()
    .reset_index()
    .rename(columns={"index": "condition", "count": "frequency"})
)
frequencies.head()
```

<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>condition</th>
      <th>frequency</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>birth control</td>
      <td>27655</td>
    </tr>
    <tr>
      <th>1</th>
      <td>depression</td>
      <td>8023</td>
    </tr>
    <tr>
      <th>2</th>
      <td>acne</td>
      <td>5209</td>
    </tr>
    <tr>
      <th>3</th>
      <td>anxiety</td>
      <td>4991</td>
    </tr>
    <tr>
      <th>4</th>
      <td>pain</td>
      <td>4744</td>
    </tr>
  </tbody>
</table>

Và khi chúng ta hoàn thành phân tích Pandas của mình, chúng ta luôn có thể tạo một đối tượng `Dataset` mới bằng cách sử dụng hàm `Dataset.from_pandas()` như sau:

```py
from datasets import Dataset

freq_dataset = Dataset.from_pandas(frequencies)
freq_dataset
```

```python out
Dataset({
    features: ['condition', 'frequency'],
    num_rows: 819
})
```

> [!TIP]
> ✏️ **Thử nghiệm thôi!** Tính xếp hạng trung bình cho mỗi loại thuốc và lưu trữ kết quả ở dạng `Dataset` mới.

Phần này kết thúc chuyến tham quan của chúng ta về các kỹ thuật tiền xử lý khác nhau có sẵn trong 🤗 Datasets. Để hoàn thiện phần này, hãy tạo một tệp kiểm định để chuẩn bị tập dữ liệu cho việc huấn luyện một trình phân loại. Trước khi làm như vậy, chúng ta sẽ đặt lại định dạng đầu ra của `drug_dataset` từ `"pandas"` thành `"arrow"`:

```python
drug_dataset.reset_format()
```

## Tạo ra một tệp kiểm định

Mặc dù chúng ta có một bộ dữ liệu kiểm thử có thể sử dụng để đánh giá, nhưng bạn nên giữ nguyên bộ kiểm thử và tạo một bộ kiểm định riêng trong quá trình phát triển. Khi bạn hài lòng với hiệu suất của các mô hình của mình trên bộ kiểm định, bạn có thể thực hiện kiểm tra lần cuối đối với bộ kiểm thử. Quy trình này giúp giảm thiểu rủi ro rằng bạn sẽ trang bị quá mức cho bộ kiểm thử và triển khai một mô hình không thành công trên dữ liệu trong thế giới thực.

🤗 Datasets cung cấp một hàm `Dataset.train_test_split()` dựa trên tính năng nổi tiếng từ `scikit-learn`. Hãy cùng dùng nó để chia tập huấn luyện thành các tập `train` và `validation` (ta đặt tham số `seed` cho mục đính tái tạo):

```py
drug_dataset_clean = drug_dataset["train"].train_test_split(train_size=0.8, seed=42)
# Thay đổi tên mặc định "test" thành "validation"
drug_dataset_clean["validation"] = drug_dataset_clean.pop("test")
# Thêm "test" vào `DatasetDict`
drug_dataset_clean["test"] = drug_dataset["test"]
drug_dataset_clean
```

```python out
DatasetDict({
    train: Dataset({
        features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount', 'review_length', 'review_clean'],
        num_rows: 110811
    })
    validation: Dataset({
        features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount', 'review_length', 'review_clean'],
        num_rows: 27703
    })
    test: Dataset({
        features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount', 'review_length', 'review_clean'],
        num_rows: 46108
    })
})
```

Tuyệt vời, ta hiện đã chuẩn bị một tập dữ liệu sẵn sàng để huấn luyện một số mô hình! Trong [phần 5](/course/chapter5/5), chúng tôi sẽ chỉ cho bạn cách tải tập dữ liệu lên Hugging Face Hub, nhưng bây giờ hãy quen với phân tích của chúng tôi bằng cách xem xét một số cách bạn có thể lưu tập dữ liệu trên máy cục bộ của mình.

## Lưu một bộ dữ liệu

<Youtube id="blF9uxYcKHo"/>

Mặc dù 🤗 Datasets sẽ lưu vào bộ nhớ cache mọi tập dữ liệu đã tải xuống và các hoạt động được thực hiện trên nó, nhưng đôi khi bạn sẽ muốn lưu tập dữ liệu vào đĩa (ví dụ: trong trường hợp bộ nhớ cache bị xóa). Như thể hiện trong bảng bên dưới, 🤗 Datasets cung cấp ba chức năng chính để lưu tập dữ liệu của bạn ở các định dạng khác nhau:

| Định dạng dữ liệu |        Hàm        |
| :---------: | :--------------------: |
|    Arrow    | `Dataset.save_to_disk()` |
|     CSV     |    `Dataset.to_csv()`    |
|    JSON     |   `Dataset.to_json()`    |

Ví dụ, hãy cùng lưu dữ liệu sạch của chúng ta về định dạng Arrow:

```py
drug_dataset_clean.save_to_disk("drug-reviews")
```

Nó sẽ tạo ra một kho lưu trữ với cấu trúc như sau:

```
drug-reviews/
├── dataset_dict.json
├── test
│   ├── dataset.arrow
│   ├── dataset_info.json
│   └── state.json
├── train
│   ├── dataset.arrow
│   ├── dataset_info.json
│   ├── indices.arrow
│   └── state.json
└── validation
    ├── dataset.arrow
    ├── dataset_info.json
    ├── indices.arrow
    └── state.json
```

nơi chúng ta có thể thấy rằng mỗi phần tách ra được liên kết với bảng *dataset.arrow* của riêng nó và một số siêu dữ liệu trong *dataset_info.json* và *state.json*. Bạn có thể coi định dạng Arrow như một bảng gồm các cột và hàng ưa thích được tối ưu hóa để xây dựng các ứng dụng hiệu suất cao xử lý và vận chuyển các tập dữ liệu lớn.

Sau khi tập dữ liệu được lưu, chúng ta có thể tải nó bằng cách sử dụng hàm `load_from_disk()` như sau:

```py
from datasets import load_from_disk

drug_dataset_reloaded = load_from_disk("drug-reviews")
drug_dataset_reloaded
```

```python out
DatasetDict({
    train: Dataset({
        features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount', 'review_length'],
        num_rows: 110811
    })
    validation: Dataset({
        features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount', 'review_length'],
        num_rows: 27703
    })
    test: Dataset({
        features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount', 'review_length'],
        num_rows: 46108
    })
})
```
Đối với định dạng CSV và JSON, chúng ta phải lưu trữ từng phần thành một tệp riêng biệt. Một cách để làm điều này là lặp lại các khóa và giá trị trong đối tượng `DatasetDict`:

```py
for split, dataset in drug_dataset_clean.items():
    dataset.to_json(f"drug-reviews-{split}.jsonl")
```

Nó sẽ lưu mỗi phần dữ liệu vào[định dạng JSON Lines](https://jsonlines.org), nơi mỗi dòng trong bộ dữ liệu được lưu trữ trên một dòng JSON. Đây là một ví dụ về hình hài cua nó:

```py
!head -n 1 drug-reviews-train.jsonl
```

```python out
{"patient_id":141780,"drugName":"Escitalopram","condition":"depression","review":"\"I seemed to experience the regular side effects of LEXAPRO, insomnia, low sex drive, sleepiness during the day. I am taking it at night because my doctor said if it made me tired to take it at night. I assumed it would and started out taking it at night. Strange dreams, some pleasant. I was diagnosed with fibromyalgia. Seems to be helping with the pain. Have had anxiety and depression in my family, and have tried quite a few other medications that haven't worked. Only have been on it for two weeks but feel more positive in my mind, want to accomplish more in my life. Hopefully the side effects will dwindle away, worth it to stick with it from hearing others responses. Great medication.\"","rating":9.0,"date":"May 29, 2011","usefulCount":10,"review_length":125}
```

Chúng ta sau đó có thể sử dụng các kỹ thuật trong [phần 2](/course/chapter5/2) để tải tệp JSON như sau:

```py
data_files = {
    "train": "drug-reviews-train.jsonl",
    "validation": "drug-reviews-validation.jsonl",
    "test": "drug-reviews-test.jsonl",
}
drug_dataset_reloaded = load_dataset("json", data_files=data_files)
```

Và đó là nó cho chuyến du ngoạn của chúng ta với sắp xếp dữ liệu sử dụng 🤗 Datasets! Giờ ta đã có một tập dữ liệu đã được làm sạch để huấn luyện mô hình, đây là một vài ý tưởng mà bạn có thể thử:

1. Sử dụng các kỹ thuật từ [Chương 3](/course/chapter3) để huấn luyện một bộ phân loại có thể dự đoán tình trạng bệnh nhân dựa trên các phản hồi về thuốc.
2. Sử dụng pipeline `summarization` từ [Chương 1](/course/chapter1) để tạo các bản tóm tắt các bài đánh giá.

Tiếp theo, chúng ta sẽ xem xét cách 🤗 Datasets có thể cho phép bạn làm việc với những tập dữ liệu khổng lồ mà không làm hỏng máy tính xách tay của bạn!
